package chess;

import java.awt.event.*;
import java.io.*;
import java.net.*;
import java.util.*;
import java.awt.Desktop;
import javax.swing.*;

import pieces.*;
import players.Player;
import model.*;
import view.*;

public class Controller implements ActionListener, MouseListener
{
	private ChessBoard model;
	private MainView view;
	private SetupMenu setupWindow;
	private String currentGame;
	private PrintWriter saveFile;
	
	public static void main(String[] args)
	{ 
		@SuppressWarnings("unused")
		Controller main = new Controller();
	}
	/**
	 * Constructs either a new instance of Chess or loads an old game.
	 */
	public Controller()
	{
		view = new MainView(this, "config/Skin Settings.cfg", false);
		
		Object[] options = {"New Game", "Load Game"};
		int n = JOptionPane.showOptionDialog(null, "What would you like to do?",
				"New Game", JOptionPane.DEFAULT_OPTION, JOptionPane.QUESTION_MESSAGE, null, options, options[0]);
		if (n == 0)
			setupWindow = new SetupMenu(this, true);
		else if(n == 1)
		{
			String path = view.showFileChooser();
			try {
				load(path.substring(path.indexOf("Saves")));
				view.setVisible(true);
			}
			catch(NullPointerException e) {}
		}
		else
			System.exit(0);
		
	}

	/**
	 * Overrides actionPerformed(ActionEvent) as defined in {@link java.awt.Event.ActionListener}. Directs each
	 * event to the appropriate handler method.
	 * 
	 * @param event an {@link java.awt.event.ActionEvent} fired from one of many different {@link view} items.
	 */
	public void actionPerformed(ActionEvent event)
	{
		Object src = event.getSource();
		if (src instanceof JMenuItem)
			menuClicked((JMenuItem)src);
		else if (src instanceof JRadioButton)
			radioClicked((JRadioButton)src, event.getActionCommand());
		else if (event.getActionCommand().equals("exit initial setup"))
			exitInitialSetup();
		else if (event.getActionCommand().equals("exit skin setup"))
			setupWindow.dispose();	
	}
	
	/**
	 * Overrides mousePressed(MouseEvent) as defined in {@link java.awt.Event.MouseListener}. Directs presses on
	 * {@link view.BoardPanel}'s to {@link panelClicked}. The result of this direction determines whether sound will be played.
	 * 
	 * @param event a {@link java.awt.event.MouseEvent} fired from a {@link javax.swing.JPanel}, or an extending class.
	 */
	public void mousePressed(MouseEvent event) 
	{
		Object src = event.getSource();
		if (src instanceof BoardPanel)
		{
			if(panelClicked((BoardPanel)src, true))
				view.playFX("click.wav");
		}
	}

	/**
	 * Finalizes initial settings from the {@link view.SetupMenu}.
	 */
	private void exitInitialSetup() 
	{
		String[] allSettings = setupWindow.getAllSettings();
		if (allSettings[5].equals("") || allSettings[5].equals(".sav"))
		{
			JOptionPane.showMessageDialog(setupWindow, "Must designate a game name!");
			return;
		}
		else if (Integer.parseInt(allSettings[0]) > 1 && Integer.parseInt(allSettings[0]) < 5)
		{
			JOptionPane.showMessageDialog(setupWindow, "Please select game type!");
			return;
		}
		setupWindow.dispose();
		
		currentGame = allSettings[5];
		
		switch (Integer.parseInt(allSettings[0])) {
			case 1:	model = new ChessBoard(ChessBoard.HUMAN_HUMAN, false);
					break;
			case 5: model = new ChessBoard(ChessBoard.HUMAN_WHITE_COMP, false);
					break;
			case 6: model = new ChessBoard(ChessBoard.HUMAN_WHITE_COMP, true);
					break;
			case 7: model = new ChessBoard(ChessBoard.HUMAN_BLACK_COMP, false);
					break;
			case 8: model = new ChessBoard(ChessBoard.HUMAN_BLACK_COMP, true);
					break;
		}
		try 
		{
			saveFile = new PrintWriter("Saves/"+currentGame);
			saveFile.println("2");
			saveFile.println("1");
			saveFile.println("");
			for (int x = 0; x < allSettings.length; x++)
				saveFile.println(allSettings[x]);
			saveFile.println("");
			saveFile.println("!0");
		} catch (IOException e) {}
		if ((int) model.getSettings()[0] == ChessBoard.HUMAN_BLACK_COMP)
			computerMove();
		displayCurrentGrid(model.getGrid());
		view.setVisible(true);
	}
	
	/**
	 * Forces the {@link view} to display the current state of the {@link model}.
	 * 
	 * @param initialSetup a reference to the current state of the {@link model}
	 */
	private void displayCurrentGrid(Object[][] initialSetup) 
	{
		for (int x = 0; x < initialSetup.length; x++)
		{
			for (int y = 0; y < initialSetup[0].length; y++)
			{
				Piece temp = (Piece) initialSetup[x][y];				
				if (temp!=null)
					view.setPiece(temp.getLocation(), temp.getID(), temp.getColor());
			}
		}
	}
	
	/**
	 * The handler for {@link java.awt.event.MouseEvent}'s. Works by checking two cases: whether the object at the source
	 * of the press is null or not. Depending on the case, different actions are taken to update the {@link model} and {@link view}.
	 * @param src the {@link view.BoardPanel} that was clicked
	 * @param useAI whether or not this game is using an {@link players.ComputerPlayer}
	 * @return to play sound, or not to play sound
	 */
	private boolean panelClicked(BoardPanel src, boolean useAI)
	{
		Location clicked = src.getGridLocation();
		Piece piece = model.getObjectAt(clicked);
		Piece currentSelect = model.getSelectedPiece();
		if (piece != null)
		{
			if (currentSelect != null && piece.getColor() != model.getCurrentPlayer().getColor() && model.getMoveLocations(currentSelect).contains(clicked))
			{
				Location from = currentSelect.getLocation();
				Location to = src.getGridLocation();
				view.highlight(model.getMoveLocations(currentSelect), false);
				view.movePiece(from, to);
				handleModelResult(model.processInput(from, to), from, to);
				model.deselectPiece();
				save(from, to);
				if (model.getCurrentPlayer() instanceof players.ComputerPlayer && useAI)
					computerMove();
				return true;
			}
			else if (currentSelect != null && piece.getColor() != model.getCurrentPlayer().getColor() && !model.getMoveLocations(currentSelect).contains(clicked))
				view.displayText("You can't move there!");
			else if (piece.getColor( ) == model.getCurrentPlayer().getColor())
			{
				view.highlight(model.getMoveLocations(currentSelect), false);
				model.selectPiece(piece);
				view.highlight(model.getMoveLocations(piece), true);
				return true;
			}
			//return;
		}
		else
		{
			Piece selected = model.getSelectedPiece();
			if (selected != null && model.getMoveLocations(selected).contains(src.getGridLocation()))
			{
				piece = model.getSelectedPiece();
				view.highlight(model.getMoveLocations(piece), false);
				Location from = piece.getLocation();
				Location to = src.getGridLocation();
				view.movePiece(from, to);
				handleModelResult(model.processInput(from, to), from, to);
				save(from, to);
				if (model.getCurrentPlayer() instanceof players.ComputerPlayer && useAI)
					computerMove();
				model.deselectPiece();
				return true;
			}
			else if (selected != null && !model.getMoveLocations(selected).contains(src.getGridLocation()))
				view.displayText("You can't move there!!");
			else
				view.displayText("There is nothing there! :P");
		}
		return false;
	}
	
	/**
	 * Used in loading a saved game.
	 * @param from the {@link model.Location} that was moved from
	 * @param to the {@link model.Location} that was moved to
	 */
	private void loadHumanMove(Location from, Location to)
	{
		handleModelResult(model.processInput(from, to), from, to);
		view.movePiece(from, to);
	}
	
	/**
	 * Parses the result of a move from {@link model.ChessBoard}.
	 * @param modelResult the result of the move, from {@link model.ChessBoard.processInput}
	 * @param from the {@link model.Location} moved from
	 * @param to the {@link model.Locatoin} moved to
	 */
	private void handleModelResult(String[] modelResult, Location from, Location to)
	{
		String color = "Black", otherColor = "White";
		if (model.getCurrentPlayer().getColor() == Player.BLACK)
		{
			color = "White";
			otherColor = "Black";
		}
		switch(modelResult[0]) 
		{
		case "Checked": 
			view.displayText(color + " " + modelResult[0] + " " + otherColor);
			break;
		case "Moved":				
			String translated = " from " +translate(from) + " to " + translate(to);
			view.displayText(color + " " + modelResult[0] + translated);
			break;
		case "Promote": 
			int promotion = 0;
			if (model.getCurrentPlayer() instanceof players.HumanPlayer)
				promotion = view.askForPromotion();
			else
				promotion = pieces.Piece.QUEEN;
			model.promote((Pawn) model.getObjectAt(to), promotion);
			view.setPiece(to, promotion, model.getObjectAt(to).getColor());
			view.displayText(otherColor + " Promotion");
			break;
		case "Stalemate":
			if (modelResult[0].equals("Stalemmate"))
				view.displayText("Stalemate");
			else
				view.displayText(modelResult[0]);
			showEndgame(modelResult[0]);
			break;
		case "Checkmate":
			if (modelResult[0].equals("Checkmate"))
				view.displayText(color + " Checkmated " + otherColor);
			else
				view.displayText(modelResult[0]);
			showEndgame(modelResult[0]);
			break;
		default: //For "Castle"
			String loc = modelResult[0].substring(6);
			Location rookTo = Location.stringToLoc(loc);
			Location rookFrom = null;
			if (rookTo.getCol() == 5)
				rookFrom = new Location(rookTo.getRow(), 7);
			else
				rookFrom = new Location(rookTo.getRow(), 0);
			view.movePiece(rookFrom, rookTo);
			view.displayText(color + " Castle");
			save(rookFrom, rookTo);
			break;
		}
	}
	
	/**
	 * Translates a {@link model.Location} to traditional Chess layout systems.
	 * @param loc the {@link model.Location} to be translated
	 * @return a {java.lang.String} that is the translation
	 */
	private String translate(Location loc)
	{
		String translated = "";
		int col = loc.getCol();
		switch (col)
		{
		case 0:
			translated += "A";
			break;
		case 1:
			translated += "B";
			break;
		case 2:
			translated += "C";
			break;
		case 3:
			translated += "D";
			break;
		case 4:
			translated += "E";
			break;
		case 5:
			translated += "F";
			break;
		case 6:
			translated += "G";
			break;
		case 7:
			translated += "H";
			break;
		}
		translated += (8 - loc.getRow());
		return translated;
	}
	@SuppressWarnings("static-access")
	/**
	 * Handles the end of a Chess game.
	 * @param result the result of the last move
	 */
	private void showEndgame(String result)
	{
		int gameType = (int) model.getSettings()[0];
		if (result.equals("Checkmate") && gameType == model.HUMAN_HUMAN)
			view.playFX("win.wav");
		else
			view.playFX("error.wav");
		Object[] options = {"Play again", "Quit"};
		int n = JOptionPane.showOptionDialog(view, result + ". What would you like to do?",
				"Game Over", JOptionPane.DEFAULT_OPTION, JOptionPane.QUESTION_MESSAGE, null, options, options[0]);
		if (n == 0)
			this.newGame();
		else
			System.exit(0);
	}
	/**
	 * For ease of access. Calls {@link computerMove(Location, Location}.
	 */
	private void computerMove()
	{
		computerMove(null, null);
	}
	
	/**
	 * Tells the {@link players.ComputerPlayer} to move, or asks it to move.
	 * @param from null if asking, otherwise telling
	 * @param to null if asking, otherwise telling
	 */
	private void computerMove(Location from, Location to)
	{
		// if from and to are null this generates a move for the ComputerPlayer
		String[] modelResult = model.getCurrentPlayer().makeMove(from, to);
		if (from == null || to == null)
		{
			from = Location.stringToLoc(modelResult[1].substring(0, modelResult[1].indexOf(',')));
			to = Location.stringToLoc(modelResult[1].substring(modelResult[1].indexOf(',')+2));
		}
		view.movePiece(from, to);
		handleModelResult(modelResult, from, to);
		save(from, to);
	}
	
	/**
	 * Handles {@link java.awt.event.ActionEvent}s that occur in the {@link javax.swing.JMenu}.
	 * @param src the {@link javax.swing.JMenuItem} that was clicked
	 */
	private void menuClicked(JMenuItem src)
	{
		String label = src.getText();

		switch(label)
		{
		case "New Game":
			int newResult = JOptionPane.showConfirmDialog(view, "Do you want to start a new game?", "New Game", JOptionPane.YES_NO_OPTION);
			if (newResult == JOptionPane.CLOSED_OPTION || newResult == JOptionPane.NO_OPTION)
				return;
			else
				this.newGame();
			break;
		case "Load Game":
			String path = view.showFileChooser();
			try {
				load(path.substring(path.indexOf("Saves")));
			}
			catch(NullPointerException e) {}
			break;
		case "Save & Quit":
			int quitResult = JOptionPane.showConfirmDialog(view, "Are you sure you want to quit?", "Quit", JOptionPane.YES_NO_OPTION);
			if (quitResult == JOptionPane.CLOSED_OPTION || quitResult == JOptionPane.NO_OPTION)
				return;
			else
			{
				saveFile.println("!0");
				saveFile.close();
				System.exit(0);
			}
			break;
		case "Undo Move":
			if(((Integer)model.getSettings()[0])!= 1)
			{
				undo();
				undo();
			}
			else
				undo();
			break;
		case "Redo Move":
			if(((Integer)model.getSettings()[0])!= 1)
			{
				redo();
				redo();
			}
			else
				redo();
			break;
		case "Skin Settings":
			setupWindow = new SetupMenu(this, false);
			break;
		case "Learn to Play":
			try {
				Desktop.getDesktop().browse(new URI("http://en.wikipedia.org/wiki/Chess"));
			}
			catch (Exception e) {}
			break;
		}
	}
	
	/**
	 * Handles {@link java.awt.event.ActionEvent}s from the {@link view.SetupMenu}'s {@link chess.RadioGroup}s.
	 * @param src the {@link javax.swing.JRadioButton} that was pressed
	 * @param actionCommand the {@link java.lang.String} command message associated with the button
	 */
	private void radioClicked(JRadioButton src, String actionCommand)
	{
		int item = Integer.parseInt(actionCommand.substring(0,1));
		int command = Integer.parseInt(actionCommand.substring(1));

		setupWindow.recordSetting(item, command);
		
		if (!(command > 4 && command < 12))
			setupWindow.changeDisplay(item, command);
		
		if (item!=0)
			view.changeDisplaySettings(item, command);
	}
	
	/**
	 * This method autosaves each move to the save file.
	 * @param from {@link model.Location} moved from
	 * @param to {@link model.Location} moved to
	 */
	private void save(Location from, Location to)
	{
		saveFile.println(from.toString());
		saveFile.println(to.toString());
	}
	
	/**
	 * Loads from a save file.
	 * @param save the name of the save to be loaded
	 */
	private void load(String save)
	{
		if(save== null)
			return;
		
		//data[0] holds settings
		//data[1] holds from and to locations, alternating respectively
		ArrayList<String>[][] data = GroupsHelper.importData(save);
		ArrayList<String> loadedSettings = data[0][0];
		ArrayList<String> loadedMoves = data[0][1];
		LinkedList<Location> moves = parseMoves(loadedMoves);
		int gametype = Integer.parseInt(loadedSettings.get(0));
		model = newBoard(gametype);
		//ArrayList<String> viewSettings = loadedSettings.get(1);
		for (int x = 1; x < loadedSettings.size()-1; x++)
			view.changeDisplaySettings(x, Integer.parseInt(loadedSettings.get(x)));
		displayCurrentGrid(model.getGrid());
		String filename = save.substring(6);
		currentGame = filename;
		File delete = new File(currentGame);
		delete.delete();;
		try {
			saveFile = new PrintWriter("Saves/"+currentGame);
			saveFile.println("2");
			saveFile.println("1");
			saveFile.println("");
			for (int x = 0; x < loadedSettings.size(); x++)
				saveFile.println(loadedSettings.get(x));
			saveFile.println("");
			saveFile.println("!0");
		} catch (IOException e) {
			System.out.println("Could not find directory");
			e.printStackTrace();
		}
		gametype = (Integer)model.getSettings()[0];
		Iterator<Location> iter = moves.iterator();
		if (gametype == 1)
		{	
			while (iter.hasNext())
			{
				Location from = iter.next();
				Location to = iter.next();
				panelClicked(new BoardPanel(from), false);
				panelClicked(new BoardPanel(to), false);
			}
		}
		else if (gametype == 2 || gametype == 3)
		{
			while (iter.hasNext())
			{
				Location from = iter.next();
				Location to = iter.next();
				if (model.getCurrentPlayer() instanceof players.ComputerPlayer)
					computerMove(from, to);
				else
					loadHumanMove(from, to);
			}
		}
		view.clearText();
	}
	
	/**
	 * A {@link load} helper method.
	 * @param gametype the gametype of the save file
	 * @return a blank {@link model.ChessBoard} of the appropriate gametype
	 */
	private ChessBoard newBoard(int gametype)
	{
		ChessBoard newModel = null;
		if (gametype == 1)
			newModel = new ChessBoard(ChessBoard.HUMAN_HUMAN, false);
		else if (gametype == 5)
			newModel = new ChessBoard(ChessBoard.HUMAN_WHITE_COMP, false);
		else if (gametype == 6)
			newModel = new ChessBoard(ChessBoard.HUMAN_WHITE_COMP, true);
		else if (gametype == 7)
			newModel = new ChessBoard(ChessBoard.HUMAN_BLACK_COMP, false);
		else if (gametype == 8)
			newModel = new ChessBoard(ChessBoard.HUMAN_BLACK_COMP, true);
		return newModel;
	}
	
	/**
	 * {@link load} helper method. Parses the saved moves into a more readable format.
	 * @param loadedMoves the loaded moves from the save file
	 * @return a {@link java.util.LinkedList} of {@link model.Location}s which better represents the saved game
	 */
	private LinkedList<Location> parseMoves(ArrayList<String> loadedMoves)
	{
		LinkedList<Location> moves = new LinkedList<>();
		for (int x = 0; x < loadedMoves.size(); x++)
		{
			if (x % 2 == 0)
				moves.add(Location.stringToLoc(loadedMoves.get(x)));
			else
				moves.add(Location.stringToLoc(loadedMoves.get(x)));
		}
		return moves;
	}
	
	/**
	 * Undoes the last move, if possible.
	 */
	public void undo()
	{
		Move move = model.getUndo();
		if (move==null)
			return;
		
		if (model.getSelectedPiece()!=null)
		{
			view.highlight(model.getMoveLocations(model.getSelectedPiece()), false);
			model.deselectPiece();
		}
		model.set(((Piece) move.getFromPiece()), ((Location) move.getFrom()));
		if (((Piece) move.getFromPiece()) != null)
			((Piece) move.getFromPiece()).setLocation(((Location) move.getFrom()));
		
		model.set(((Piece) move.getToPiece()), ((Location) move.getTo()));
		if (((Piece) move.getToPiece()) != null)
			((Piece) move.getToPiece()).setLocation(((Location) move.getTo()));
		
		try
		{
			view.setPiece(((Location) move.getFrom()), ((Piece) move.getFromPiece()).getID(), ((Piece) move.getFromPiece()).getColor());
		}
		catch(NullPointerException e)
		{
			view.setPiece(((Location) move.getFrom()), -1, -1);
		}
		
		try
		{
			view.setPiece(((Location) move.getTo()), ((Piece) move.getToPiece()).getID(), ((Piece) move.getToPiece()).getColor());
		}
		catch(NullPointerException e)
		{
			view.setPiece(((Location) move.getTo()), -1, -1);
		}
		model.changeColor();
		
	}
	
	/**
	 * Redoes the last undone move, if possible.
	 */
	public void redo()
	{
		Move move = model.getRedo();
		if (move==null)
		{
			return;
		}
		
		if (model.getSelectedPiece()!=null)
		{
			view.highlight(model.getMoveLocations(model.getSelectedPiece()), false);
			model.deselectPiece();
		}
		
		model.set(null, ((Location) move.getFrom()));
		model.set(((Piece) move.getFromPiece()), ((Location) move.getTo()));
		if (((Piece) move.getFromPiece()) != null)
			((Piece) move.getFromPiece()).setLocation(((Location) move.getTo()));

		view.setPiece(((Location) move.getFrom()), -1, -1);
		
		try
		{
			view.setPiece(((Location) move.getTo()), ((Piece) move.getFromPiece()).getID(), ((Piece) move.getFromPiece()).getColor());
		}
		catch(NullPointerException e)
		{
			view.setPiece(((Location) move.getTo()), -1, -1);
		}
		model.changeColor();
	}
	
	/**
	 * Resets all variables and restarts the program.
	 */
	private void newGame()
	{
		view.dispose();
		model = null;
		currentGame = null;
		saveFile = null;
		Controller.main(null);
	}
	@Override
	public void mouseEntered(MouseEvent event) {}
	@Override
	public void mouseExited(MouseEvent event) {}
	@Override
	public void mouseClicked(MouseEvent event) {}
	@Override
	public void mouseReleased(MouseEvent event) {}
}
